Skip to content

Fix Motion Tracker keyframes not applying after save/reload#1854

Merged
ddennedy merged 2 commits into
mltframework:masterfrom
skuznetsov:fix/motion-tracker-clock-keyframes
Jun 29, 2026
Merged

Fix Motion Tracker keyframes not applying after save/reload#1854
ddennedy merged 2 commits into
mltframework:masterfrom
skuznetsov:fix/motion-tracker-clock-keyframes

Conversation

@skuznetsov

@skuznetsov skuznetsov commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Problem

After saving and reopening a project, Load Keyframes from Motion Tracker silently applies zero keyframes — the masked rectangle stays static instead of following the tracked motion.

Root cause

MotionTrackerModel::trackingData() parses each keyframe's time key with QString::toInt, which only accepts integer frame numbers (e.g. 5). When a project is saved, Shotcut serializes animated properties — including the opencv.tracker results — in clock time format (e.g. 00:00:00.167). After a save + reload, toInt then fails on every entry, so trackingData returns an empty list and applyTracking lays down no keyframes.

This is why tracking "works" right after Analyze (in-memory results are frame-numbered) but stops working once the project has been saved and reopened.

Fix

Accept both frame-number and clock/timecode time keys. The parsed frame value is not used downstream (only the rectangles are consumed by applyTracking/reset), so fall back to the running index when toInt fails.

Verification

On a real project whose tracker results were stored as clock timecodes, the parser yielded 0 rectangles before the change and all 46 after, and the masked filter's keyframes are applied across the whole clip as expected.

MotionTrackerModel::trackingData parsed each keyframe's time key with
QString::toInt, which only accepts integer frame numbers ("5"). When a
project is saved, Shotcut serializes animated properties (including the
opencv.tracker `results`) in clock time format (00:00:00.167). After a
save+reload, toInt then failed on every entry, trackingData returned an
empty list, and "Load Keyframes from Motion Tracker" silently applied
zero keyframes (the masked rect stayed static).

Accept both frame-number and clock/timecode keys. The parsed frame value
is not used downstream (only the rectangles are consumed by applyTracking
and reset), so fall back to the running index when toInt fails.
@skuznetsov skuznetsov force-pushed the fix/motion-tracker-clock-keyframes branch from 843c062 to 4b45983 Compare June 27, 2026 13:36
Comment thread src/models/motiontrackermodel.cpp Outdated
// downstream, so the exact frame value is not significant here.
int frame = pair.at(0).toInt(&ok);
if (!ok)
frame = int(result.size());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should provide clear steps to reproduce the bug since I did not reproduce it. I think there was a change in the past year or so to ensure property animations would save as time values instead of frame numbers to ensure they adapt to changing the project video mode's frame rate. So, it would not surprise me if this bug occurred as a result. However, I must be able to confirm a fix.

I added a debug log line inside this method to log each pair. The MLT XML contains

<property name="results">00:00:00.000~=1333 671 192 108 0;00:00:00.167~=1334 669 192 108 0;00:00:00.334~=1332 664 196 110 0;00:00:00.501~=1335 661 192 108 0;00:00:00.667~=1335 657 196 110 0;00:00:00.834~=1338 655 192 108 0;00:00:01.001~=1337 652 196 110 0;00:00:01.168~=1339 649 196 110 0;00:00:01.335~=1342 649 192 108 0;00:00:01.502~=1342 646 196 110 0;00:00:01.668~=1343 646 196 110 0;00:00:01.835~=1342 646 196 110 0;00:00:02.002~=1345 647 192 108 0;00:00:02.169~=1345 648 192 108 0;00:00:02.336~=1345 650 192 108 0;00:00:02.503~=1344 650 192 108 0;00:00:02.669~=1343 652 192 108 0;00:00:02.836~=1342 653 192 108 0;00:00:03.003~=1340 654 192 108 0;00:00:03.170~=1339 654 192 108 0;00:00:03.203~=1339 654 192 108 0</property>

And the debug log shows:

[Debug  ] <MotionTrackerModel::trackingData> QList("0", "1333 671 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("5", "1334 669 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("10", "1332 664 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("15", "1335 661 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("20", "1335 657 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("25", "1338 655 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("30", "1337 652 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("35", "1339 649 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("40", "1342 649 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("45", "1342 646 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("50", "1343 646 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("55", "1342 646 196 110 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("60", "1345 647 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("65", "1345 648 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("70", "1345 650 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("75", "1344 650 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("80", "1343 652 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("85", "1342 653 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("90", "1340 654 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("95", "1339 654 192 108 0") 
[Debug  ] <MotionTrackerModel::trackingData> QList("96", "1339 654 192 108 0") 

The first pair item is already a numeric string. It appears something already converted them from time values.

Secondly, your change is wrong; it cannot simply use the index as the frame number because it currently makes a keyframe every 5 frames. It needs to use MLT->consumer()->time_to_frames().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review.
Reproduction (it only triggers once the results are read back as clock time, which is what happens here after a save + reload):

  1. Add a Motion Tracker filter to a clip, set the rect, and Analyze.
  2. Add a Mask: Simple Shape filter and use "Load Keyframes from Motion Tracker" - this works.
  3. Save the project, close it, and reopen it.
  4. Select the mask and "Load Keyframes from Motion Tracker" again → zero keyframes are applied (the masked rect stays static).

On reopen the saved results are in clock form, e.g. 00:00:00.000~=789 457 154 13 0;00:00:00.167~=..., and trackingData() receives those clock strings. The original code did pair.at(0).toInt(&ok) and only appended the item if (ok). toInt() fails on 00:00:00.167, so every entry was skipped and trackingData() returned an empty list, so no keyframes applied. That if (ok) gate is the real bug.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those are the steps I am doing but not reproducing it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

Responding to review feedback: rather than falling back to the running
index when QString::toInt fails on a clock-time key, parse the keyframe
time with Mlt::Consumer::time_to_frames. It converts both the clock form
("00:00:00.167") and the frame-number form ("5") to the correct frame
using the project frame rate, so a project reloaded with clock-time
results no longer yields an empty list and the keyframes apply.
@ddennedy ddennedy added this to the 26.7 milestone Jun 29, 2026
@ddennedy ddennedy merged commit 8b555f6 into mltframework:master Jun 29, 2026
2 of 3 checks passed
@ddennedy

Copy link
Copy Markdown
Member

Thank you. Even though I did not reproduce it, I do believe there are ways it can get to that part of the code with time values and this is a good, valuable fix that does not regress.

@skuznetsov

Copy link
Copy Markdown
Contributor Author

My apologies for not answering earlier.
I found it hard way, when I downloaded ShotCut, and could not understand why the positions are not saving. So I had to clone the repo and go through the code with a debugger to see what is going on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants